﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;

namespace Creventive.Wpf.ShellFactory.Workspace.Primitives
{
    /// <summary>
    /// A handler for items sources, for use within custom controls using the ItemsSourceCollection.
    /// </summary>
    public interface IItemsSourceHandler
    {
        /// <summary>
        /// Sets the source of an items source.
        /// </summary>
        /// <param name="source">The source.</param>
        void SetSource(IEnumerable source);
    }

    /// <summary>
    /// Represents a collection of application view sources which can function either as a collection on its own or as a router system.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ItemsSourceCollection<T> : IList<T>, IList, INotifyCollectionChanged
    {
        private class ItemsSourceHandler: IItemsSourceHandler
        {
            private readonly ItemsSourceCollection<T> collection;

            public ItemsSourceHandler(ItemsSourceCollection<T> collection)
            {
                this.collection = collection;
            }

            public void SetSource(IEnumerable source)
            {
                this.collection.SetSource(source);
            }
        }

        /// <summary>
        /// Creates a collection and its associated handler.
        /// </summary>
        /// <param name="collection">The collection.</param>
        /// <param name="handler">The handler.</param>
        public static void Create(out ItemsSourceCollection<T> collection, out IItemsSourceHandler handler)
        {
            collection = new ItemsSourceCollection<T>();
            handler = new ItemsSourceHandler(collection);
        }

        private IEnumerable source;
        private List<T> items = new List<T>();

        /// <summary>
        /// Occurs when the collection changes.
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        /// <summary>
        ///   Gets the number of view sources in this collection.
        /// </summary>
        /// <value>The number of view sources in this collectio.n</value>
        public int Count
        {
            get { return this.items.Count; }
        }

        bool IList.IsFixedSize
        {
            get { return false; }
        }

        bool ICollection<T>.IsReadOnly
        {
            get { return this.IsSourceApplied; }
        }

        bool IList.IsReadOnly
        {
            get { return ((ICollection<T>)this).IsReadOnly; }
        }

        /// <summary>
        /// Gets a value indicating whether this collection has a source applied to it.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this collection has a source applied to it; otherwise, <c>false</c>.
        /// </value>
        public bool IsSourceApplied
        {
            get { return this.source != null; }
        }

        bool ICollection.IsSynchronized
        {
            get { return false; }
        }

        object IList.this[int index]
        {
            get { return this[index]; }
            set { this[index] = (T)value; }
        }

        /// <summary>
        /// Gets or sets the item at the specified index.
        /// </summary>
        /// <value></value>
        public T this[int index]
        {
            get { return this.items[index]; }
            set
            {
                this.EnsureNoSourceApplied();
                this.SetItem(index, value);
            }
        }

        object ICollection.SyncRoot
        {
            get { return null; }
        }

        private void SetSource(IEnumerable source)
        {
            this.EnsureCanApplySource();
            if (this.source == source)
                return;
            var oldSource = this.source;
            this.source = source;
            this.OnSourceChanged(oldSource, source);
        }

        /// <summary>
        /// Called when the Source property has changed.
        /// </summary>
        /// <param name="oldSource">The old source.</param>
        /// <param name="newSource">The new source.</param>
        protected virtual void OnSourceChanged(IEnumerable oldSource, IEnumerable newSource)
        {
            if (oldSource is INotifyCollectionChanged)
                ((INotifyCollectionChanged)oldSource).CollectionChanged -= this.source_CollectionChanged;
            if (newSource != null)
            {
                if (newSource is INotifyCollectionChanged)
                    ((INotifyCollectionChanged)newSource).CollectionChanged += this.source_CollectionChanged;
                this.source_CollectionChanged(newSource, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
            else
            {
                this.items.Clear();
                this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

        private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if ((e.OldItems != null && e.OldItems.Count > 1) || e.NewItems != null && e.NewItems.Count > 1)
                throw new InvalidOperationException(this.GetType().Name + " does not support batched operations.");

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                {
                    int index = e.NewStartingIndex;
                    if (index == -1)
                        index = this.Count;
                    T item = (T)e.NewItems[0];
                    this.InsertItem(index, item);
                    break;
                }

                case NotifyCollectionChangedAction.Move:
                {
                    int oldIndex = e.OldStartingIndex;
                    T oldItem = (T)e.OldItems[0];
                    if (oldIndex == -1)
                        oldIndex = this.items.IndexOf(oldItem);
                    int newIndex = e.NewStartingIndex;
                    this.MoveItem(oldIndex, newIndex, oldItem);
                    break;
                }

                case NotifyCollectionChangedAction.Remove:
                {
                    int index = e.OldStartingIndex;
                    T item = (T)e.OldItems[0];
                    if (index == -1)
                        index = this.items.IndexOf(item);
                    this.RemoveItem(index, item);
                    break;
                }

                case NotifyCollectionChangedAction.Replace:
                {
                    int index = e.OldStartingIndex;
                    T oldItem = (T)e.OldItems[0];
                    T newItem = (T)e.NewItems[0];
                    if (index == -1)
                        index = this.items.IndexOf(oldItem);
                    this.SetItem(index, newItem);
                    break;
                }

                case NotifyCollectionChangedAction.Reset:
                    this.items.Clear();
                    this.items.AddRange((IEnumerable<T>)sender);
                    this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    break;

                default:
                    throw new IndexOutOfRangeException();
            }
        }

        /// <summary>
        /// Ensures that a source can be applied to this collection.
        /// </summary>
        protected void EnsureCanApplySource()
        {
            if (this.IsSourceApplied || this.Count == 0)
                return;
            else
                throw new InvalidOperationException("Cannot apply a source when collection has been manually modified");
        }

        /// <summary>
        /// Ensures that no source has been applied to this collection.
        /// </summary>
        protected void EnsureNoSourceApplied()
        {
            if (this.IsSourceApplied)
                throw new InvalidOperationException("Cannot modify directly when a source has been applied");
        }

        /// <summary>
        /// Gets the enumerator.
        /// </summary>
        /// <returns></returns>
        public IEnumerator<T> GetEnumerator()
        {
            return this.items.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Adds the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        public void Add(T item)
        {
            this.EnsureNoSourceApplied();
            this.InsertItem(this.Count, item);
        }

        int IList.Add(object value)
        {
            this.Add((T)value);
            return this.Count - 1;
        }

        bool IList.Contains(object value)
        {
            return this.Contains((T)value);
        }

        /// <summary>
        /// Clears this collection.
        /// </summary>
        public void Clear()
        {
            this.EnsureNoSourceApplied();
            this.ClearItems();
        }

        int IList.IndexOf(object value)
        {
            return this.IndexOf((T)value);
        }

        void IList.Insert(int index, object value)
        {
            this.Insert(index, (T)value);
        }

        void IList.Remove(object value)
        {
            this.Remove((T)value);
        }

        /// <summary>
        /// Determines whether this collection contains the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>
        /// 	<c>true</c> if this collection contains the specified item; otherwise, <c>false</c>.
        /// </returns>
        public bool Contains(T item)
        {
            return this.items.Contains(item);
        }

        /// <summary>
        /// Copies the contents of this collection into the specified array.
        /// </summary>
        /// <param name="array">The array.</param>
        /// <param name="arrayIndex">Index of the array.</param>
        public void CopyTo(T[] array, int arrayIndex)
        {
            this.items.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Removes the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns></returns>
        public bool Remove(T item)
        {
            this.EnsureNoSourceApplied();
            var index = this.items.IndexOf(item);
            if (index == -1)
                return false;
            this.RemoveItem(index, item);
            return true;
        }

        void ICollection.CopyTo(Array array, int index)
        {
            this.CopyTo((T[])array, index);
        }

        /// <summary>
        /// Determines the index of the specified item. Returns -1 if the item does not exist in this collection.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns></returns>
        public int IndexOf(T item)
        {
            return this.items.IndexOf(item);
        }

        /// <summary>
        /// Inserts an item at the specified index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="item">The item.</param>
        public void Insert(int index, T item)
        {
            this.EnsureNoSourceApplied();
            this.InsertItem(index, item);
        }

        /// <summary>
        /// Removes the item at the specified index.
        /// </summary>
        /// <param name="index">The index.</param>
        public void RemoveAt(int index)
        {
            this.EnsureNoSourceApplied();
            this.RemoveItem(index, this.items[index]);
        }

        /// <summary>
        /// Moves the item.
        /// </summary>
        /// <param name="oldIndex">The old index.</param>
        /// <param name="newIndex">The new index.</param>
        /// <param name="item">The item.</param>
        protected virtual void MoveItem(int oldIndex, int newIndex, T item)
        {
            this.items.RemoveAt(oldIndex);
            // Adjust index if necessary
            if (newIndex > oldIndex)
                newIndex--;
            this.items.Insert(newIndex, item);
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
        }

        /// <summary>
        /// Sets the item.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="item">The item.</param>
        protected virtual void SetItem(int index, T item)
        {
            var oldItem = this.items[index];
            this.items[index] = item;
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem, index));
        }

        /// <summary>
        /// Clears the items.
        /// </summary>
        protected virtual void ClearItems()
        {
            this.items.Clear();
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        /// <summary>
        /// Inserts the item.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="item">The item.</param>
        protected virtual void InsertItem(int index, T item)
        {
            this.items.Insert(index, item);
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }

        /// <summary>
        /// Removes the item.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="item">The item.</param>
        protected virtual void RemoveItem(int index, T item)
        {
            this.items.RemoveAt(index);
            this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
        }

        /// <summary>
        /// Raises the <see cref="CollectionChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (this.CollectionChanged != null)
                this.CollectionChanged(this, e);
        }
    }
}